/*
 * Copyright 2007-2008 Volker Fritzsch
 *
 * Licensed under the Apache License, Version 2.0 (the "License");
 * you may not use this file except in compliance with the License.
 * You may obtain a copy of the License at
 *
 *     http://www.apache.org/licenses/LICENSE-2.0
 *
 * Unless required by applicable law or agreed to in writing, software
 * distributed under the License is distributed on an "AS IS" BASIS,
 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
 * See the License for the specific language governing permissions and
 * limitations under the License. 
 */
package motej;

import javax.swing.event.EventListenerList;

import motej.centore.NewLogger;
import motej.event.AccelerometerEvent;
import motej.event.AccelerometerListener;
import motej.event.CoreButtonEvent;
import motej.event.CoreButtonListener;
import motej.event.DataEvent;
import motej.event.DataListener;
import motej.event.ExtensionEvent;
import motej.event.ExtensionListener;
import motej.event.IrCameraEvent;
import motej.event.IrCameraListener;
import motej.event.MoteDisconnectedEvent;
import motej.event.MoteDisconnectedListener;
import motej.event.StatusInformationListener;
import motej.request.CalibrationDataRequest;
import motej.request.PlayerLedRequest;
import motej.request.RawByteRequest;
import motej.request.ReadRegisterRequest;
import motej.request.ReportModeRequest;
import motej.request.RumbleRequest;
import motej.request.StatusInformationRequest;
import motej.request.WriteRegisterRequest;

import org.slf4j.Logger;
import org.slf4j.LoggerFactory;

import com.intel.bluetooth.BlueCoveImpl;

/**
 * 
 * <p>
 * @author <a href="mailto:vfritzsch@users.sourceforge.net">Volker Fritzsch</a>
 * @author Daniel Centore
 */
public class Mote
{

	private Logger log = LoggerFactory.getLogger(Mote.class);
	private OutgoingThread outgoing;
	private IncomingThread incoming;
	private ExtensionProvider extensionProvider = new ExtensionProvider();
	private Extension currentExtension;
	private StatusInformationReport statusInformationReport;
	private CalibrationDataReport calibrationDataReport;
	private EventListenerList listenerList = new EventListenerList();
	private String bluetoothAddress;
	private Object dongle;

	public Mote(String bluetoothAddress, Object id, MoteFinder moteFinder)
	{
		try
		{
			dongle = id;

			this.bluetoothAddress = bluetoothAddress;

			// I'm interested if one of the Thread is disconnected
			addMoteDisconnectedListener(new MoteDisconnectedListener<Mote>()
			{

				public void moteDisconnected(MoteDisconnectedEvent<Mote> evt)
				{
					// Something goes wrong with one of my Thread
					// Try to properly cleanup everything
					disconnect();
				}
			});

			outgoing = new OutgoingThread(this, bluetoothAddress, id);
			incoming = new IncomingThread(this, bluetoothAddress);

			incoming.start();
			outgoing.start();

			outgoing.sendRequest(new StatusInformationRequest());
			outgoing.sendRequest(new CalibrationDataRequest());
		} catch (Exception ex)
		{
			NewLogger.println("Bad WiiMote connection request.");
			ex.printStackTrace();
			throw new RuntimeException(ex.fillInStackTrace());
		}
	}

	public void addAccelerometerListener(AccelerometerListener<Mote> listener)
	{
		listenerList.add(AccelerometerListener.class, listener);
	}

	public void addCoreButtonListener(CoreButtonListener listener)
	{
		listenerList.add(CoreButtonListener.class, listener);
	}

	public void addDataListener(DataListener listener)
	{
		listenerList.add(DataListener.class, listener);
	}

	public void addExtensionListener(ExtensionListener listener)
	{
		listenerList.add(ExtensionListener.class, listener);
	}

	public void addIrCameraListener(IrCameraListener listener)
	{
		listenerList.add(IrCameraListener.class, listener);
	}

	public void addMoteDisconnectedListener(MoteDisconnectedListener<Mote> listener)
	{
		listenerList.add(MoteDisconnectedListener.class, listener);
	}

	public void addStatusInformationListener(StatusInformationListener listener)
	{
		listenerList.add(StatusInformationListener.class, listener);
	}

	public void disableIrCamera()
	{
		// 1. Disable IR Camera
		outgoing.sendRequest(new RawByteRequest(new byte[] { 82, 19, 0 }));

		// 2. Disable IR Camera 2
		outgoing.sendRequest(new RawByteRequest(new byte[] { 82, 26, 0 }));
	}

	public void disconnect()
	{
		NewLogger.println("Connection with WiiMote forcibly closed.");
		
		MoteFinder.getMoteFinder().bluetoothAddressCache.remove(bluetoothAddress);

		if (outgoing != null)
		{
			outgoing.disconnect();
			try
			{
				outgoing.join(5000l);
			} catch (InterruptedException ex)
			{
				log.error(ex.getMessage(), ex);
			}
		}
		if (incoming != null)
		{
			incoming.disconnect();
			try
			{
				incoming.join(5000l);
			} catch (InterruptedException ex)
			{
				log.error(ex.getMessage(), ex);
			}
		}
	}

	/**
	 * Enables the IR Camera in basic mode with Marcan sensitivity.
	 */
	public void enableIrCamera()
	{
		enableIrCamera(IrCameraMode.BASIC, IrCameraSensitivity.WII_LEVEL_3);
	}

	public void enableIrCamera(IrCameraMode mode, IrCameraSensitivity sensitivity)
	{
		// 1. Enable IR Pixel Clock (Send 0x06 to Output Report 0x13)
		outgoing.sendRequest(new RawByteRequest(new byte[] { 82, 0x13, 0x06 }));

		// 2. Enable IR Logic (Send 0x06 to Output Report 0x1a)
		outgoing.sendRequest(new RawByteRequest(new byte[] { 82, 0x1a, 0x06 }));

		// 3. Write 0x01 to register 0xb00030
		outgoing.sendRequest(new WriteRegisterRequest(new byte[] { (byte) 0xb0, 0x00, 0x30 }, new byte[] { 0x01 }));

		// 4. Write Sensitivity Block 1 to registers at 0xb00000
		outgoing.sendRequest(new WriteRegisterRequest(new byte[] { (byte) 0xb0, 0x00, 0x00 }, sensitivity.block1()));

		// 5. Write Sensitivity Block 2 to registers at 0xb0001a
		outgoing.sendRequest(new WriteRegisterRequest(new byte[] { (byte) 0xb0, 0x00, 0x1a }, sensitivity.block2()));

		// 6. Write Mode Number to register 0xb00033
		outgoing.sendRequest(new WriteRegisterRequest(new byte[] { (byte) 0xb0, 0x00, 0x33 }, new byte[] { mode.modeAsByte() }));

		// 7. Write 0x08 to register 0xb00030
		outgoing.sendRequest(new WriteRegisterRequest(new byte[] { (byte) 0xb0, 0x00, 0x30 }, new byte[] { 0x08 }));
	}

	@Override
	public boolean equals(Object obj)
	{
		if (!(obj instanceof Mote))
			return false;

		return hashCode() == obj.hashCode();
	}

	@SuppressWarnings("unchecked")
	protected void fireMoteDisconnectedEvent()
	{
		MoteDisconnectedListener<Mote>[] listeners = listenerList.getListeners(MoteDisconnectedListener.class);
		MoteDisconnectedEvent<Mote> evt = new MoteDisconnectedEvent<Mote>(this);
		for (MoteDisconnectedListener<Mote> l : listeners)
		{
			l.moteDisconnected(evt);
		}
	}

	@SuppressWarnings("unchecked")
	protected void fireAccelerometerEvent(int x, int y, int z)
	{
		AccelerometerListener<Mote>[] listeners = listenerList.getListeners(AccelerometerListener.class);
		AccelerometerEvent<Mote> evt = new AccelerometerEvent<Mote>(this, x, y, z);
		for (AccelerometerListener<Mote> l : listeners)
		{
			l.accelerometerChanged(evt);
		}
	}

	protected void fireCoreButtonEvent(int modifiers)
	{
		CoreButtonListener[] listeners = listenerList.getListeners(CoreButtonListener.class);
		CoreButtonEvent evt = new CoreButtonEvent(this, modifiers);
		for (CoreButtonListener l : listeners)
		{
			l.buttonPressed(evt);
		}
	}

	protected void fireExtensionConnectedEvent()
	{
		ExtensionListener[] listeners = listenerList.getListeners(ExtensionListener.class);
		ExtensionEvent evt = new ExtensionEvent(this, currentExtension);
		for (ExtensionListener l : listeners)
		{
			l.extensionConnected(evt);
		}
	}

	protected void fireExtensionDisconnectedEvent()
	{
		ExtensionListener[] listeners = listenerList.getListeners(ExtensionListener.class);
		ExtensionEvent evt = new ExtensionEvent(this);
		for (ExtensionListener l : listeners)
		{
			l.extensionDisconnected(evt);
		}
	}

	protected void fireIrCameraEvent(IrCameraMode mode, IrPoint p0, IrPoint p1, IrPoint p2, IrPoint p3)
	{
		IrCameraListener[] listeners = listenerList.getListeners(IrCameraListener.class);
		IrCameraEvent evt = new IrCameraEvent(this, mode, p0, p1, p2, p3);
		for (IrCameraListener l : listeners)
		{
			l.irImageChanged(evt);
		}
	}

	protected void fireReadDataEvent(byte[] address, byte[] payload, int error)
	{
		if (calibrationDataReport == null && error == 0 && address[0] == 0x00 && address[1] == 0x20)
		{
			// calibration data (most probably)
			if (log.isDebugEnabled())
			{
				log.debug("Received Calibration Data Report.");
			}
			CalibrationDataReport report = new CalibrationDataReport(payload[0] & 0xff, payload[1] & 0xff, payload[2] & 0xff, payload[4] & 0xff, payload[5] & 0xff, payload[6] & 0xff);
			calibrationDataReport = report;
		}

		if (currentExtension == null && error == 0 && address[0] == 0x00 && (address[1] & 0xff) == 0xfe && payload.length == 2)
		{
			// extension ID (most probably)
			if (log.isDebugEnabled())
			{
				String id0 = Integer.toHexString(payload[0] & 0xff);
				String id1 = Integer.toHexString(payload[1] & 0xff);
				log.debug("Received Extension ID: " + (id0.length() == 1 ? "0x0" + id0 : "0x" + id0) + " " + (id1.length() == 1 ? "0x0" + id1 : "0x" + id1));
			}

			if ((payload[0] & 0xff) == 0xff && (payload[1] & 0xff) == 0xff)
			{
				log.debug("Connection not completed, re-requesting extension id.");
				outgoing.sendRequest(new ReadRegisterRequest(new byte[] { (byte) 0xa4, 0x00, (byte) 0xfe }, new byte[] { 0x00, 0x02 }));
			}
			else
			{

				currentExtension = extensionProvider.getExtension(payload);
				if (log.isInfoEnabled())
				{
					log.info("Found extension: " + currentExtension == null ? "null" : currentExtension.toString());
				}
				if (currentExtension != null)
				{
					currentExtension.setMote(this);
					currentExtension.initialize();
					incoming.setExtension(currentExtension);
					fireExtensionConnectedEvent();
				}
			}
		}

		DataListener[] listeners = listenerList.getListeners(DataListener.class);
		DataEvent evt = new DataEvent(address, payload, error);
		for (DataListener l : listeners)
		{
			l.dataRead(evt);
		}
	}

	protected void fireStatusInformationChangedEvent(StatusInformationReport report)
	{
		// decide if we should query the extension port
		boolean extensionChanged;
		if (statusInformationReport == null)
		{
			extensionChanged = report.isExtensionControllerConnected();
		}
		else
		{
			extensionChanged = statusInformationReport.isExtensionControllerConnected() != report.isExtensionControllerConnected();
		}

		statusInformationReport = report;
		StatusInformationListener[] listeners = listenerList.getListeners(StatusInformationListener.class);
		for (StatusInformationListener l : listeners)
		{
			l.statusInformationReceived(report);
		}

		if (extensionChanged)
		{
			if (!report.isExtensionControllerConnected())
			{
				currentExtension = null;
				fireExtensionDisconnectedEvent();
			}
			else
			{
				// 1. initialize peripheral (writing zero to 0xa40040) 
				outgoing.sendRequest(new WriteRegisterRequest(new byte[] { (byte) 0xa4, 0x00, 0x40 }, new byte[] { 0x00 }));

				// 2. read extension ID bytes from wii register (0xa400fe)
				outgoing.sendRequest(new ReadRegisterRequest(new byte[] { (byte) 0xa4, 0x00, (byte) 0xfe }, new byte[] { 0x00, 0x02 }));
			}
		}
	}

	public String getBluetoothAddress()
	{
		return bluetoothAddress;
	}

	public CalibrationDataReport getCalibrationDataReport()
	{
		return calibrationDataReport;
	}

	public StatusInformationReport getStatusInformationReport()
	{
		return statusInformationReport;
	}

	@Override
	public int hashCode()
	{
		return bluetoothAddress.hashCode();
	}

	public void removeAccelerometerListener(AccelerometerListener<Mote> listener)
	{
		listenerList.remove(AccelerometerListener.class, listener);
	}

	public void removeCoreButtonListener(CoreButtonListener listener)
	{
		listenerList.remove(CoreButtonListener.class, listener);
	}

	public void removeDataListener(DataListener listener)
	{
		listenerList.remove(DataListener.class, listener);
	}

	public void removeExtensionListener(ExtensionListener listener)
	{
		listenerList.remove(ExtensionListener.class, listener);
	}

	public void removeIrCameraListener(IrCameraListener listener)
	{
		listenerList.remove(IrCameraListener.class, listener);
	}

	public void remoteMoteDisconnectedListener(MoteDisconnectedListener<Mote> listener)
	{
		listenerList.remove(MoteDisconnectedListener.class, listener);
	}

	public void removeStatusInformationListener(StatusInformationListener listener)
	{
		listenerList.remove(StatusInformationListener.class, listener);
	}

	public void requestStatusInformation()
	{
		outgoing.sendRequest(new StatusInformationRequest());
	}

	public void rumble(long millis)
	{
		outgoing.sendRequest(new RumbleRequest(millis));
	}

	public void setPlayerLeds(boolean[] leds)
	{
		if (outgoing == null)
		{
			System.out.println("VERY BAD");
			System.exit(-1);
		}
		try
		{
			outgoing.sendRequest(new PlayerLedRequest(leds));
		} catch (Exception e)
		{
			System.out.println(outgoing);
			System.out.println(leds);

			e.printStackTrace();
			System.exit(-1);
		}
	}

	public void setReportMode(byte mode)
	{
		outgoing.sendRequest(new ReportModeRequest(mode));
	}

	public void setReportMode(byte mode, boolean continuous)
	{
		outgoing.sendRequest(new ReportModeRequest(mode, continuous));
	}

	public void readRegisters(byte[] offset, byte[] size)
	{
		outgoing.sendRequest(new ReadRegisterRequest(offset, size));
	}

	public void writeRegisters(byte[] offset, byte[] payload)
	{
		outgoing.sendRequest(new WriteRegisterRequest(offset, payload));
	}

	@SuppressWarnings("unchecked")
	public <T extends Extension> T getExtension()
	{
		return (T) currentExtension;
	}

	@Override
	public String toString()
	{
		return "Mote[" + bluetoothAddress + "]";
	}

	public Object getDongle()
	{
		return dongle;
	}
}
